古灵精怪的python——地址,浅拷贝与身份运算符
作者:DeepWeaver Python爱好者社区专栏作者
知乎专栏:深度学堂
https://zhuanlan.zhihu.com/c_172487736
首先抛出一个问题,吸引读者的阅读兴趣(如果您觉得这个不是问题,那么这篇文章不适合您:)
请看如下代码:
>>> a = 3
>>> b = 3
>>> a == b
True
>>> a is b
True
# 这没问题
>>> a = 3
>>> b = a
>>> a == b
True
>>> a is b
True
# 这看起来也很合理
>>> a = (2,3)
>>> b = (2,3)
>>> a is b
False # ???why?
>>> a == b
True
>>> b = a
>>> a is b
True # ???why?
>>> a == b
True
好了,整篇文章都是围绕这个问题展开的。长久以来我都习惯用is
而不用==
来进行两个对象的比较(python中一切皆对象) 直到今天出了一个bug后才了解到这两者之间的不同,挖到python的一个大坑之余,不禁出了一身冷汗。。。
用is
还是==
?
补充知识
id() 用于获取对象在内存中的地址,并以十进制展示出来。如:
>>> a = 3
>>> id(a)
140602638349720
>>> hex(id(a)) # 还原成我们看着更顺眼的16进制,但是本文以10进制地址为主(因为懒)
'0x7fe09a503598'
顾名思义,is是“相同”,而==是指两者之间的”相等“关系。所谓相同,比较的是两者之间的在内存中的位置,
>>> a = 3
>>> id(a)
140602638349720
>>> b = 3 # b指向的是和a指向的同一块地址(但是并不意味这改变了a,b也会相应改变)
>>> id(b)
140602638349720
>>> c = a # a的引用复制给c,在内存中其实是指向了用一个对象
>>> id(c)
140602638349720
>>> a is b
True
>>> a is c
True
>>> b is c
True
我们看到,上面a,b,c的地址相同,所以他们互相之间”相同“
而相等则两者之间的数值对应相等
>>> a = 3
>>> b = a
>>> a = 4
>>> b
3
>>> a = [3]
>>> b = [3]
>>> id(a)
4351374184
>>> id(b)
4351374112
>>> a is b
False
>>> a == b
True
>>> a[0] = 4
>>> b
[3]
>>> a = [3]
>>> b = a # b就是a的引用,占得是同一块地址,而且当a的内容改变时,b也会随之改变,这和上面
# int对象不同,我也不知道为啥要这么搞。
>>> a[0] = 4
>>> b
[4]
很多同学看到这肯定是一锅浆糊了,其实就是一个原则,能用==就不用is。除了一种情况,那就是判断对象是否是None。
>>> if a is None:
... pass
浅拷贝和深拷贝
>>> a = [3]
>>> b = a[:] #通过切片赋值,返回的是a的浅拷贝
>>> id(a)
4351273944
>>> id(b)
4351374184
>>> id(a[0])
140602638349720
>>> id(b[0]) #list的地址不同
140602638349720
>>> a is b
False
>>> a[0] is b[0] #浅拷贝,只拷贝了a的壳[],里边的内容仍然是同一个东西,同样的id
True
>>> a == b
True
>>> a[0] == b[0]
True
>>> a[0] = 4 # 但是,b的内容不会随着a的变化而变化
>>> b
[3]
浅拷贝拷贝了最外层容器,副本中的元素是原容器中元素的引用
我们再看一个例子
>>> Anndy = ['Anndy', ['age', 24]]
>>> Tom = Anndy[:]
>>> Cindy = list(Anndy)
>>> id(Anndy)
4351374040
>>> id(Tom)
4351373968
>>> id(Cindy)
4351374616
>>> print(Anndy, Tom, Cindy)
(['Anndy', ['age', 24]],['Anndy', ['age', 24]],['Anndy', ['age', 24]])
# 看起来是创建了三个不同的对象,因为他们的id各不相同
>>> Tom[0] = 'Tom'
>>> Cindy[0] = 'Cindy'
>>> print (Anndy, Tom, Cindy)
(['Anndy', ['age', 24]], ['Tom', ['age', 24]], ['Cindy', ['age', 24]])
# 如果想修改某一个人的名字也没有什么问题
# 现在我们想把Tom的年龄修改为12岁
>>> Tom[1][1] = 12
>>> print (Anndy, Tom, Cindy)
(['Anndy', ['age', 12]], ['Tom', ['age', 12]], ['Cindy', ['age', 12]])
# 震惊!所有人的年龄都变成了12!!!
>>> print ([id(x) for x in Anndy])
[4351366368, 4351374112] # 看第二个列表的地址
>>> print ([id(x) for x in Tom])
[4351323592, 4351374112] # 看第二个!
>>> print ([id(x) for x in Cindy])
[4351366224, 4351374112] # 第一个姓名元素的地址不同,但是第二个列表是同一个
构造方法或切片 [:] 做的是浅拷贝。如果所有元素都是不可变的(比如名字字符串,修改的时候会重新创建对象,仅仅包括原子对象的元组也属于这种情况),那么这样没有问题,还能节省内存。但是,如果有可变的元素,可能就会导致意想不到的问题,正如刚刚,修改一个人的年龄,所有人的年龄都发生了变化。
所以,如果你想要深拷贝,应该这么写
>>> import copy
>>> Anndy = ['Anndy', ['age', 24]]
>>> Tom = copy.deepcopy(Anndy)
>>> Tom[1][1] = 12
>>> print(Tom, Anndy)
(['Anndy', ['age', 12]], ['Anndy', ['age', 24]]) #这样写就没问题了
另外
不知道刚才你有没有注意到
>>> a = 3
>>> b = 3
>>> id(a)
140602638349720
>>> id(b)
140602638349720 # 相同!
>>> a is b
True
Python会对比较小的整数对象进行缓存缓存起来。当整数比较大的时候就会重新开辟一块内存。
>>> a = 999
>>> b = 999
>>> id(a)
140602638469952
>>> id(b)
140602638469904 # 不同!
>>> a is b
False
这仅仅是在命令行中执行,而在保存为文件执行,结果是不一样的,这是因为解释器做了一部分优化。
#!/usr/bin/env python
a = 3
b = 3
print(a is b)
a = 99999
b = 99999
print(a is b)
结果:
True
True
[Finished in 0.0s]
这也是为什么我屡屡用is而不用==,程序运行良好的原因。
总结
1. python中,尽量不要用is
, 除非判断对象是否为None
。
2. a is b
(相同)一定意味着a == b
(相等),而a == b
(相等) 不一定 a is b
(相同)这点比较好理解
3. 如果函数中传参等,需要引用、拷贝的,注意是否生成了一个新的对象,即使生成了,内部元素是否是同一个对象的引用?尤其注意切片的使用。必要的时候用copy模块进行深拷贝而不要用切片这种浅拷贝形式。
Python爱好者社区历史文章大合集:
Python爱好者社区历史文章列表(每周append更新一次)
关注后在公众号内回复“课程”即可获取:
小编的Python入门免费视频课程!!!
【最新免费微课】小编的Python快速上手matplotlib可视化库!!!
崔老师爬虫实战案例免费学习视频。
陈老师数据分析报告制作免费学习视频。
玩转大数据分析!Spark2.X+Python 精华实战课程免费学习视频。